#include <arch.h>
#include <cpu.h>
#include <conio.h>
#include <multiboot.h>
#include <mman.h>
#include <schedule.h>
#include <paging.h>
#include <string.h>
#include <sys/unistd.h>

extern sysinfo_t sys;

uint32_t virt_to_page(uint32_t virt, uint32_t pdir)
{
    return (PAGE_ALIGN(get_pte(virt, pdir)));
}

uint32_t virt_to_phys(uint32_t virt, uint32_t pdir)
{
    uint32_t page;
    uint32_t addr;
    uint32_t offset;

    page = virt_to_page(virt, pdir);
    if (!page)
	return 0;

    addr = PAGE_ALIGN(virt);
    offset = (virt - addr);
    page += offset;

    return (page);
}

void *copy_fromuser(uint32_t user_addr, void *dst, 
    uint32_t proc_pdir, size_t nbytes)
{
    uint32_t from_addr;
    void *res = NULL;
    uint32_t to_addr = (uint32_t)dst;   /* kernel addr is phys! */
    
    if (user_addr == 0)
        return NULL;
        
    if (dst == NULL)
        return NULL;
        
    if (proc_pdir == 0)
        return NULL;
    
    sched_lock();
    from_addr = virt_to_phys(user_addr, proc_pdir);
    res = memcpy((void *)from_addr, (void *)to_addr, nbytes);
    sched_unlock();
    
    return (res);
}

void *copy_touser(uint32_t kern_addr, void *dst,
    uint32_t proc_pdir, size_t nbytes)
{
    uint32_t to_addr;
    void *res = NULL;
    
    if (kern_addr == 0)
        return NULL;
        
    if (dst == NULL)
        return NULL;
        
    if (proc_pdir == 0)
        return NULL;
        
    sched_lock();
    to_addr = virt_to_phys((uint32_t)dst, proc_pdir);
    res = memcpy((void *)kern_addr, (void *)to_addr, nbytes);
    sched_unlock();
    
    return (res);
}

addr_t mem_convert(addr_t pdir, addr_t vaddr, size_t size)
{
	addr_t addr, pte, pa;
	addr_t start, end, pg;

	sched_lock();
	start = PAGE_ALIGN(vaddr);
	end = PAGE_ALIGN(vaddr + size - 1);

	/* check all pages exist */
	for(pg = start; pg <= end; pg += PAGE_SIZE) {
		if ((pte = (addr_t)get_pde(pg, pdir)) == 0) {
			sched_unlock();
			
			return 0;
		}
		if (get_pte(pte, pg) == 0) {
			sched_unlock();
			
			return 0;
		}
	};

	pa = (addr_t)get_pte((uint32_t)pte, (uint32_t)start);
	addr = (pa + vaddr - start);
	
	return (addr);
}

void paging_init(void)
{
    uint32_t pagedir;
    
    pagedir = build_pagedir();
    identity_map((uint32_t *)pagedir, PV_KERNEL | PV_WR | PV_PRESENT);

    load_cr3(pagedir);
    enable_paging();

    sys.kernel_pdir = pagedir;
}

void identity_map(uint32_t *pagedir, uint32_t pflags)
{
    int i;
    int memory_pages;
    multiboot_info_t *mbi;

    mbi = arch_getmbi();        /* todo: map corrected mem ranges for kernel! */
    memory_pages = (mbi->mem_lower + mbi->mem_upper) * 0x400 / PAGE_SIZE;
    map_pages(0x00000000, 0x00000000, (addr_t)pagedir, 
    	memory_pages * PAGE_SIZE, pflags);
}

uint32_t build_pagedir(void)
{
    uint32_t *page_dir;
    int i;

    page_dir = (uint32_t *)alloc_page();
    if (page_dir == NULL)
    	return 0;

    kprintf("page directory at 0x%p\n", page_dir);

    for (i = 0; i < 1024; i++) {
		page_dir[i] = 0;
    }

    return ((uint32_t)page_dir);
}

addr_t new_pagedir(void)
{
	uint32_t addr = 0;

	if ((addr = build_pagedir()) == 0)
		return 0;
	
	return (copy_pagedir(addr, sys.kernel_pdir));
}

int copy_pagedir(addr_t new_pagedir_addr, addr_t old_pagedir_addr)
{
    int i;
    uint32_t *new_pagedir, *old_pagedir;
    uint32_t table_addr;
    uint32_t new_table_addr;
    uint32_t table_flags;
    uint32_t pde, pte;

    new_pagedir = (uint32_t *) new_pagedir_addr;
    old_pagedir = (uint32_t *) old_pagedir_addr;

    for (i = 0; i < 1024; i++) {
		/* determine PDE */
		pde = old_pagedir[i];

		if (pde & PV_PRESENT) {
	    	/* if page table is present
	       		determine page table address & flags */
	    	table_addr = pde & 0xFFFFF000;
	    	table_flags = pde & 0x00000FFF;

	    	new_table_addr = alloc_page();

	    	copy_pagetable(new_table_addr, table_addr);
	    	new_pagedir[i] = new_table_addr | table_flags;
		} else {
	    	/* if page table is not present */
	    	new_pagedir[i] = old_pagedir[i];
		}
    }

    return 0;
}

int copy_pagetable(addr_t new_table_addr, addr_t old_table_addr)
{
    int i;
    uint32_t *new_pagetable, *old_pagetable;
    uint32_t pte;

    new_pagetable = (uint32_t *) new_table_addr;
    old_pagetable = (uint32_t *) old_table_addr;

    /* todo: copying pages */
    for (i = 0; i < 1024; i++) {
		pte = old_pagetable[i];
		if (pte & PV_PRESENT) {
	    	/* page present */
/*      new_pagetable[i]=(old_pagetable[i] & 0xFFFFFFFD)|VM_PAGE_COPY_ON_WRITE;
      old_pagetable[i] = (old_pagetable[i] & 0xFFFFFFFD)|VM_PAGE_COPY_ON_WRITE;*/
		} else {
	    	/* page isn't present */      
	    	new_pagetable[i] = old_pagetable[i];
		}
    }

    return 0;
}

uint32_t get_pte(uint32_t from, uint32_t pagedir_addr)
{
    uint32_t page_dir_index, page_table_index;
    uint32_t pagetable, page_table_entry;
    uint32_t *page_table_addr;
    uint32_t *page_dir = (uint32_t *) pagedir_addr;

    /* high 10 bits of page address are index in the page directory */
    page_dir_index = from >> 22;
    /* bits 12-21 are index in the page table */
    page_table_index = (from >> 12) & 0x3ff;
    /* low 12 bits are out of our interest
       (those are offset in the page)      */
    page_table_entry = (uint32_t) page_dir[page_dir_index];

    pagetable = page_table_entry >> 12;

    if (pagetable == 0) {
		return 0;
    } else {
		page_table_addr = (uint32_t *) (pagetable << 12);
    }

    return (page_table_addr[page_table_index]);
}

void set_pde(uint32_t from, uint32_t pagedir_addr, uint32_t value)
{
    uint32_t page_dir_index, page_table_index;
    uint32_t pagetable, page_table_entry;
    uint32_t *page_table_addr;
    uint32_t *page_dir = (unsigned int *) pagedir_addr;

    /* high 10 bits of page address are index in the page directory */
    page_dir_index = from >> 22;
    /* bits 12-21 are index in the page table */
    page_table_index = (from >> 12) & 0x3ff;

    page_dir[page_dir_index] = value;
}

uint32_t get_pde(uint32_t from, uint32_t pagedir_addr)
{
    uint32_t page_dir_index, page_table_index;
    uint32_t pagetable, page_table_entry;
    uint32_t *page_table_addr;
    uint32_t *page_dir = (uint32_t *) pagedir_addr;

    /* high 10 bits of page address are index in the page directory */
    page_dir_index = from >> 22;
    /* bits 12-21 are index in the page table */
    page_table_index = (from >> 12) & 0x3ff;

    return (page_dir[page_dir_index]);
}

void set_pte(uint32_t from, uint32_t pagedir_addr, uint32_t pde_flags,
	     uint32_t value)
{
    uint32_t page_dir_index, page_table_index;
    uint32_t pagetable, page_table_entry;
    uint32_t *page_table_addr;
    uint32_t *page_dir = (uint32_t *) pagedir_addr;

    /* high 10 bits of page address are index in the page directory */
    page_dir_index = from >> 22;
    /* bits 12-21 are index in the page table */
    page_table_index = (from >> 12) & 0x3ff;
    /* low 12 bits are out of our interest
       (those are offset in the page)      */
    page_table_entry = (uint32_t) page_dir[page_dir_index];
    pagetable = page_table_entry >> 12;

    if (pagetable == 0) {
		page_table_addr = (uint32_t *)alloc_page();
		memsetl(page_table_addr, 0, 1024);
		page_dir[page_dir_index] = (uint32_t)page_table_addr | 0x07;
    } else {
		page_table_addr = (uint32_t *) (pagetable << 12);
    }

    page_table_addr[page_table_index] = value;
}

/*void map_mempage(uint32_t from, uint32_t to, uint32_t pagedir_addr,
		 uint32_t pflags)
{
    set_pte(from, pagedir_addr, pflags, to | pflags);
}*/

int map_pages(addr_t from, addr_t to, addr_t pdir, 
	size_t size, uint32_t pflags)
{
	if (size == 0)
		return -1;

    int npages = size/PAGE_SIZE;
    int i = 0;
    uint32_t addr = (uint32_t)from, vaddr = (uint32_t)to;

    for(i = 0;i < npages; i++) {
    	set_pte(addr, pdir, pflags, vaddr | pflags);
    	addr += PAGE_SIZE;
    	vaddr += PAGE_SIZE;  
    };

	return 0; 
}

/*void unmap_mempage(uint32_t from, uint32_t to, uint32_t pdir)
{
	set_pde(from, pdir, to | PV_PRESENT | PV_RD | PV_WR);
	set_pte(from, pdir, PV_PRESENT | PV_RD | PV_WR, 0);
}*/

int unmap_pages(addr_t from, addr_t to, addr_t pdir, size_t size)
{
	if (size == 0)
		return -1;

	int npages = size/PAGE_SIZE;
	int i = 0;
	uint32_t addr = (uint32_t)from, vaddr = (uint32_t)to;

	for(i = 0; i < npages; i++) { 	/* setup for kernel only */
		set_pde(addr, pdir, vaddr | PV_PRESENT | PV_RD | PV_WR);
		set_pte(addr, pdir, PV_PRESENT | PV_RD | PV_WR, 0);	
		addr += PAGE_SIZE;
		vaddr += PAGE_SIZE;
	};

	return 0;
}

/* reload pagedir */
void load_cr3(uint32_t pagedir)
{
    asm("movl %0, %%cr3"::"d"(pagedir));
}

void enable_paging(void)
{
    kprintf("enabling paging....");

    asm("\n  movl %cr0, %eax"
	"\n  orl $0x80000000, %eax" "\n  movl %eax, %cr0");

    kprintf("Ok.\n");
}

void free_pagetables(uint32_t *pdir)
{
    uint32_t i, j;
    uint32_t *table;;

    for (i = 512; i < 1024; i++) {
	if (pdir[i] & 1) {
	    table = pdir[i] & 0xFFFFF000;
	    for (j = 0; j < 1024; j++) {
		if (table[j] & 1) {
		    free_page(table[j] & 0xFFFFF000);
		}
	    }
	    free_page((uint32_t)table);
	}
    }
}
